package scheduler

import "time"

// ExpressionSchedule   cron表达式
type ExpressionSchedule struct {
	Second,
	Minute,
	Hour,
	Dom,
	Month,
	Dow uint64 // 秒、分、时、日、月、星期
	Location *time.Location // 时区
}

// bounds  表达式边界值
type bounds struct {
	min     uint            // 最小值
	max     uint            // 最大值
	nameMap map[string]uint // 名称
}

var (
	seconds = bounds{0, 59, nil}
	minutes = bounds{0, 59, nil}
	hours   = bounds{0, 23, nil}
	dom     = bounds{1, 31, nil}
	months  = bounds{1, 12, map[string]uint{
		"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
		"jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12,
	}}
	dow = bounds{0, 6, map[string]uint{
		"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6,
	}}
)

const (
	starBit = 1 << 63
)

func (s *ExpressionSchedule) Next(inTime time.Time) time.Time {
	origLocation := inTime.Location()
	sLocation := s.Location
	// 传入时间与本地时区不同，则转换为本地时区
	if sLocation == time.Local {
		sLocation = inTime.Location()
	}
	if sLocation != time.Local {
		inTime = inTime.In(s.Location)
	}

	inTime = inTime.Add(1*time.Second - time.Duration(inTime.Nanosecond())*time.Second)

	added := false
	// 5五年内都没有匹配到时间，则跳出
	yearLimit := inTime.Year() + 5

WRAP:
	if inTime.Year() > yearLimit {
		return time.Time{}
	}

	for 1<<uint(inTime.Month())&s.Month == 0 {
		if !added {
			added = true
			inTime = time.Date(
				inTime.Year(),
				inTime.Month(),
				1,
				0,
				0,
				0,
				0,
				sLocation)
		}
		inTime = inTime.AddDate(0, 1, 0)
		// 如果超过一年，则跳出
		if inTime.Month() == time.January {
			goto WRAP
		}
	}

	for !dayMatch(s, inTime) {
		if !added {
			added = true
			inTime = time.Date(
				inTime.Year(),
				inTime.Month(),
				inTime.Day(), 0, 0, 0, 0, sLocation)
		}
		inTime = inTime.AddDate(0, 0, 1)
		//
		if inTime.Hour() != 0 {
			if inTime.Hour() > 12 {
				inTime = inTime.Add(time.Duration(24-inTime.Hour()) * time.Hour)
			} else {
				inTime = inTime.Add(time.Duration(-inTime.Hour()) * time.Hour)
			}
		}

		if inTime.Day() == 1 {
			goto WRAP
		}
	}

	for 1<<uint(inTime.Hour())&s.Hour == 0 {
		if !added {
			added = true
			inTime = time.Date(
				inTime.Year(),
				inTime.Month(),
				inTime.Day(),
				inTime.Hour(), 0, 0, 0, sLocation)
		}
		inTime = inTime.Add(1 * time.Hour)

		if inTime.Hour() == 0 {
			goto WRAP
		}
	}

	for 1<<uint(inTime.Minute())&s.Minute == 0 {
		if !added {
			added = true
			inTime = inTime.Truncate(time.Minute)
		}
		inTime = inTime.Add(1 * time.Minute)

		if inTime.Minute() == 0 {
			goto WRAP
		}
	}

	for 1<<uint(inTime.Second())&s.Second == 0 {
		if !added {
			added = true
			inTime = inTime.Truncate(time.Second)
		}
		inTime = inTime.Add(1 * time.Second)

		if inTime.Second() == 0 {
			goto WRAP
		}
	}

	return inTime.In(origLocation)
}

// dayMatch  判断日期是否匹配
func dayMatch(s *ExpressionSchedule, inTime time.Time) bool {
	var (
		domMatch bool = 1<<uint(inTime.Day())&s.Dom > 0
		dowMatch bool = 1<<uint(inTime.Weekday())&s.Dow > 0
	)
	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
		return domMatch && dowMatch
	}
	return domMatch || dowMatch
}
